package de.lgohlke.collection;

import java.io.PrintStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import de.lgohlke.compress.CompressableValue;
import de.lgohlke.compress.ICompressor;
import de.lgohlke.compress.NullCompressor;

/**
 * <p>CompressedHashMap class.</p>
 *
 * @author lars
 * @param <T1>
 * @param <T2>
 * @version $Id: $
 */
public class CompressedHashMap<T1, T2> extends HashMap<T1, T2>
{

  private final PrintStream                                 out                      = System.out;
  /**
   * The default initial capacity - MUST be a power of two.
   */
  protected static final int                                DEFAULT_INITIAL_CAPACITY = 16;

  /**
   * The load factor used when none specified in constructor.
   */
  protected static final float                              DEFAULT_LOAD_FACTOR      = 0.75f;
  private static final long                                 serialVersionUID         = -4961968189733442850L;

  private transient ICompressor<T1>                         keyCompressor            = new NullCompressor<T1>();
  private transient ICompressor<T2>                         valueCompressor          = new NullCompressor<T2>();

  private Map<CompressableValue<T1>, CompressableValue<T2>> dataBackend;

  /**
   * <p>Constructor for CompressedHashMap.</p>
   *
   * @param initialCapacity a int.
   * @param loadFactor a float.
   * @param <T1> a T1 object.
   * @param <T2> a T2 object.
   */
  public CompressedHashMap(final int initialCapacity, final float loadFactor)
  {
    this.dataBackend = new HashMap<CompressableValue<T1>, CompressableValue<T2>>(initialCapacity, loadFactor);
  }

  /**
   * <p>Constructor for CompressedHashMap.</p>
   *
   * @param initialCapacity a int.
   */
  public CompressedHashMap(final int initialCapacity)
  {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
  }

  /**
   * <p>Constructor for CompressedHashMap.</p>
   */
  public CompressedHashMap()
  {
    this(DEFAULT_INITIAL_CAPACITY);
  }

  /** {@inheritDoc} */
  @Override
  public boolean containsKey(final Object value)
  {
    @SuppressWarnings("unchecked")
    CompressableValue<T1> compressedKeyObject = keyCompressor.compress((T1) value);
    return dataBackend.containsKey(compressedKeyObject.getValue());
  }

  /** {@inheritDoc} */
  @Override
  public boolean containsValue(final Object value)
  {
    @SuppressWarnings("unchecked")
    CompressableValue<T2> compressedValue = valueCompressor.compress((T2) value);
    return dataBackend.containsValue(compressedValue);
  }

  /** {@inheritDoc} */
  @Override
  public T2 get(final Object key)
  {
    @SuppressWarnings("unchecked")
    CompressableValue<T1> compressedKeyObject = keyCompressor.compress((T1) key);

    CompressableValue<T2> compressedValue = dataBackend.get(compressedKeyObject);
    if (compressedValue == null)
    {
      return null;
    }
    else
    {
      return valueCompressor.decompress(compressedValue);
    }
  }

  /** {@inheritDoc} */
  @Override
  public T2 put(final T1 key, final T2 value)
  {
    CompressableValue<T1> compressedKeyObject = keyCompressor.compress(key);
    CompressableValue<T2> compressedValueObject = valueCompressor.compress(value);
    CompressableValue<T2> vo = dataBackend.put(compressedKeyObject, compressedValueObject);
    return vo == null ? null : vo.getValue();
  }

  /** {@inheritDoc} */
  @Override
  public T2 remove(final Object key)
  {
    @SuppressWarnings("unchecked")
    CompressableValue<T1> compressedKey = keyCompressor.compress((T1) key);
    CompressableValue<T2> compressedValue = dataBackend.remove(compressedKey);
    return valueCompressor.decompress(compressedValue);
  }

  /**
   * <p>addKeyCompressor.</p>
   *
   * @param compressor a {@link de.lgohlke.compress.ICompressor} object.
   * @return a {@link de.lgohlke.collection.CompressedHashMap} object.
   */
  public CompressedHashMap<T1, T2> addKeyCompressor(final ICompressor<T1> compressor)
  {
    keyCompressor = compressor;
    return this;
  }

  /**
   * <p>addValueCompressor.</p>
   *
   * @param compressor a {@link de.lgohlke.compress.ICompressor} object.
   * @return a {@link de.lgohlke.collection.CompressedHashMap} object.
   */
  public CompressedHashMap<T1, T2> addValueCompressor(final ICompressor<T2> compressor)
  {
    valueCompressor = compressor;
    return this;
  }

  /**
   * <p>printStats.</p>
   */
  public void printStats()
  {
    SavedSpace saved_spaces_keys = new SavedSpace();
    SavedSpace saved_spaces_values = new SavedSpace();

    for (Entry<CompressableValue<T1>, CompressableValue<T2>> entry : dataBackend.entrySet())
    {
      saved_spaces_keys.add(keyCompressor.savedSpace(entry.getKey()));
      saved_spaces_values.add(valueCompressor.savedSpace(entry.getValue()));
    }

    out.print("Space saved [entries: " + size() + ", \n" + //
        " keys   : " + saved_spaces_keys + ", \n" + //
        " values : " + saved_spaces_values + "]\n");
  }

  /** {@inheritDoc} */
  @Override
  public int size()
  {
    return dataBackend.size();
  }

  /** {@inheritDoc} */
  @Override
  public boolean isEmpty()
  {
    return dataBackend.isEmpty();
  }

  /** {@inheritDoc} */
  @Override
  public void clear()
  {
    dataBackend.clear();
  }

  /** {@inheritDoc} */
  @Override
  public void putAll(final Map<? extends T1, ? extends T2> m)
  {
    if (!m.isEmpty())
    {
      for (Iterator<? extends Map.Entry<? extends T1, ? extends T2>> i = m.entrySet().iterator(); i.hasNext();)
      {
        Map.Entry<? extends T1, ? extends T2> e = i.next();
        put(e.getKey(), e.getValue());
      }
    }
  }

  /** {@inheritDoc} */
  @Override
  public Set<T1> keySet()
  {
    Set<T1> keyset = new HashSet<T1>(dataBackend.size());

    for (CompressableValue<T1> vo : dataBackend.keySet())
    {
      keyset.add(keyCompressor.decompress(vo));
    }

    return keyset;
  }

  /** {@inheritDoc} */
  @Override
  public Collection<T2> values()
  {
    Set<T2> values = new HashSet<T2>(dataBackend.size());

    for (CompressableValue<T2> vo : dataBackend.values())
    {
      values.add(valueCompressor.decompress(vo));
    }

    return values;
  }

  /** {@inheritDoc} */
  @Override
  public Set<java.util.Map.Entry<T1, T2>> entrySet()
  {
    Set<Entry<T1, T2>> entries = new HashSet<Entry<T1, T2>>(dataBackend.size());
    for (Entry<CompressableValue<T1>, CompressableValue<T2>> entry : dataBackend.entrySet())
    {
      MyEntry<T1, T2> uncompressedEntry = new MyEntry<T1, T2>(null, null);

      uncompressedEntry.setKey(keyCompressor.decompress(entry.getKey()));
      uncompressedEntry.setValue(valueCompressor.decompress(entry.getValue()));

      entries.add(uncompressedEntry);
    }
    return entries;
  }

  /**
   * <p>Getter for the field <code>keyCompressor</code>.</p>
   *
   * @return a {@link de.lgohlke.compress.ICompressor} object.
   */
  public ICompressor<T1> getKeyCompressor()
  {
    return keyCompressor;
  }

  /**
   * <p>Getter for the field <code>valueCompressor</code>.</p>
   *
   * @return a {@link de.lgohlke.compress.ICompressor} object.
   */
  public ICompressor<T2> getValueCompressor()
  {
    return valueCompressor;
  }

  /**
   * <p>configure.</p>
   *
   * @param map a {@link de.lgohlke.collection.CompressedHashMap} object.
   * @return a {@link de.lgohlke.collection.CompressedHashMap} object.
   */
  public CompressedHashMap<T1, T2> configure(final CompressedHashMap<T1, T2> map)
  {
    addKeyCompressor(map.getKeyCompressor());
    addValueCompressor(map.getValueCompressor());

    return this;
  }

  /**
   * <p>Getter for the field <code>dataBackend</code>.</p>
   *
   * @return a {@link java.util.Map} object.
   */
  protected Map<CompressableValue<T1>, CompressableValue<T2>> getDataBackend()
  {
    return dataBackend;
  }

  /**
   * <p>Setter for the field <code>dataBackend</code>.</p>
   *
   * @param dataBackend a {@link java.util.Map} object.
   */
  public void setDataBackend(final Map<CompressableValue<T1>, CompressableValue<T2>> dataBackend)
  {
    this.dataBackend = dataBackend;
  }

  /**
   * copy from HashMap.Entry
   * 
   * @author lars
   * @param <K>
   * @param <V>
   */
  protected static class MyEntry<T1, T2> implements Entry<T1, T2>
  {
    private T1 key;
    private T2 value;

    /**
     * Creates new entry.
     */
    MyEntry(final T1 k, final T2 v)
    {
      value = v;
      key = k;
    }

    public final T1 getKey()
    {
      return key;
    }

    public void setKey(final T1 key)
    {
      this.key = key;
    }

    public final T2 getValue()
    {
      return value;
    }

    public final T2 setValue(final T2 newValue)
    {
      T2 oldValue = value;
      value = newValue;
      return oldValue;
    }

    @Override
    public final boolean equals(final Object o)
    {
      if (!(o instanceof Map.Entry))
      {
        return false;
      }
      @SuppressWarnings("unchecked")
      Map.Entry<T1, T2> e = (Map.Entry<T1, T2>) o;
      Object k1 = getKey();
      Object k2 = e.getKey();
      if ((k1 == k2) || ((k1 != null) && k1.equals(k2)))
      {
        Object v1 = getValue();
        Object v2 = e.getValue();
        if ((v1 == v2) || ((v1 != null) && v1.equals(v2)))
        {
          return true;
        }
      }
      return false;
    }

    @Override
    public final int hashCode()
    {
      return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
    }

    @Override
    public final String toString()
    {
      return getKey() + "=" + getValue();
    }
  }
}
